package org.luxor.commons.selenium.component;

import com.google.common.collect.ImmutableMap;
import org.apache.commons.io.IOUtils;
import org.luxor.commons.selenium.config.properties.SeleniumProperties;
import org.luxor.commons.selenium.config.properties.SeleniumProxy;
import org.luxor.commons.selenium.config.properties.WindowSize;
import org.luxor.commons.selenium.constant.Browser;
import org.luxor.commons.selenium.constant.Mode;
import org.luxor.commons.selenium.exception.CreateWebDriverException;
import org.luxor.commons.selenium.exception.LaunchBrowserException;
import org.luxor.commons.selenium.exception.SeleniumException;
import org.luxor.commons.selenium.exception.StealthJsException;
import org.luxor.commons.selenium.listener.SeleniumEventListener;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.MutableCapabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeDriverService;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.firefox.GeckoDriverService;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.ie.InternetExplorerDriverService;
import org.openqa.selenium.ie.InternetExplorerOptions;
import org.openqa.selenium.remote.ExecuteMethod;
import org.openqa.selenium.remote.LocalFileDetector;
import org.openqa.selenium.remote.RemoteExecuteMethod;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.safari.SafariOptions;
import org.openqa.selenium.support.events.WebDriverEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
import java.util.*;

import static org.openqa.selenium.remote.CapabilityType.PAGE_LOAD_STRATEGY;

/**
 * Web驱动创建器
 *
 * @author Mr.yan  @date 2022/4/8
 */
public class SeleniumDriverCreator {
    private static final Logger log = LoggerFactory.getLogger(SeleniumDriverCreator.class);

    /**
     * 创建驱动
     *
     * @param seleniumProperties 全局配置参数
     * @return org.openqa.selenium.WebDriver
     */
    public SeleniumDriver creatorSeleniumDriver(SeleniumProperties seleniumProperties) {
        return creatorSeleniumDriver(seleniumProperties, null);
    }

    /**
     * 创建驱动
     *
     * @param seleniumProperties 全局配置参数
     * @param eventListeners     监听器集合
     * @return org.openqa.selenium.WebDriver
     */
    public SeleniumDriver creatorSeleniumDriver(SeleniumProperties seleniumProperties, Collection<SeleniumEventListener> eventListeners) {
        log.info("Creator Selenium WebDriver Started...");

        Assert.notNull(seleniumProperties, "Spring容器中找不到[seleniumProperties]的实例化对象!");
        Assert.notNull(seleniumProperties.getMode(), "浏览器类型[spring.selenium.mode]配置参数，不能为空");
        Assert.notNull(seleniumProperties.getBrowser(), "浏览器类型[spring.selenium.browser]配置参数，不能为空");

        if (Mode.CLUSTER.equals(seleniumProperties.getMode())) {
            Assert.hasText(seleniumProperties.getRemoteUrl(), "远程地址[spring.selenium.remote-url]配置参数，不能为空");
        } else {
            Assert.hasText(seleniumProperties.getDriver(), "驱动地址[spring.selenium.driver]配置参数，不能为空");
        }
        if (Mode.UNDETECTED.equals(seleniumProperties.getMode())) {
            if (!Browser.CHROME.equals(seleniumProperties.getBrowser())) {
                throw new IllegalArgumentException("[" + seleniumProperties.getBrowser().name() + "]浏览器类型，暂不支持" + seleniumProperties.getMode().name() + "模式");
            }
            Assert.notNull(seleniumProperties.getBinaryLocation(), "浏览器位置[spring.selenium.binaryLocation]配置参数，不能为空");
        }

        try {
            if (Mode.CLUSTER.equals(seleniumProperties.getMode())) {
                log.debug("Creator Selenium RemoteWebDriver. hubUrl: {}", seleniumProperties.getRemoteUrl());
            } else if (Mode.UNDETECTED.equals(seleniumProperties.getMode())) {
                URL url = new URL(seleniumProperties.getRemoteUrl());
                log.debug("Creator Selenium UndetectedWebDriver. debuggerAddress: {}", url.getHost() + ":" + url.getPort());
            } else {
                log.debug("Creator Selenium WebDriver. browser:{} / driver: {}", seleniumProperties.getBrowser(), seleniumProperties.getDriver());
            }

            SeleniumDriver seleniumDriver;
            // 创建客户端实例
            if (Browser.CHROME.equals(seleniumProperties.getBrowser())) {
                seleniumDriver = getChromeWebDriver(seleniumProperties);
            } else if (Browser.IE.equals(seleniumProperties.getBrowser())) {
                seleniumDriver = getInternetExplorerWebDriver(seleniumProperties);
            } else if (Browser.EDGE.equals(seleniumProperties.getBrowser())) {
                seleniumDriver = getEdgeWebDriver(seleniumProperties);
            } else if (Browser.FIREFOX.equals(seleniumProperties.getBrowser())) {
                seleniumDriver = getFirefoxWebDriver(seleniumProperties);
            } else if (Browser.SAFARI.equals(seleniumProperties.getBrowser())) {
                seleniumDriver = getSafariWebDriver(seleniumProperties);
            } else {
                throw new CreateWebDriverException("暂时不支持[" + seleniumProperties.getBrowser().name() + "]浏览器类型");
            }

            // 设置窗口大小
            WindowSize windowSize = seleniumProperties.getWindowSize();
            if (windowSize != null) {
                WebDriver.Window window = seleniumDriver.webDriver().manage().window();
                window.setSize(new Dimension(windowSize.getWidth(), windowSize.getHeight()));
            }

            // 设置等待超时时间
            WebDriver.Timeouts timeouts = seleniumDriver.webDriver().manage().timeouts();
            if (seleniumProperties.getImplicitlyWaitTimeOutSeconds() > 0) {
                timeouts.implicitlyWait(Duration.ofSeconds(seleniumProperties.getImplicitlyWaitTimeOutSeconds()));
            }
            if (seleniumProperties.getImplicitlyWaitTimeOutSeconds() > 0) {
                timeouts.pageLoadTimeout(Duration.ofSeconds(seleniumProperties.getPageLoadTimeoutSeconds()));
            }
            if (seleniumProperties.getImplicitlyWaitTimeOutSeconds() > 0) {
                timeouts.scriptTimeout(Duration.ofSeconds(seleniumProperties.getScriptTimeoutSeconds()));
            }

            // 注册事件监听器
            if (!CollectionUtils.isEmpty(eventListeners)) {
                for (WebDriverEventListener eventListener : eventListeners) {
                    log.debug("Creator SeleniumDriver. register Listener:{}", eventListener.getClass().getSimpleName());
                    seleniumDriver.register(eventListener);
                }
            }
            log.info("Creator SeleniumDriver completed. sessionId:[{}]", seleniumDriver.getSessionId());
            return seleniumDriver;
        } catch (CreateWebDriverException createException) {
            log.error("Creator SeleniumDriver fail.", createException);
            throw createException;
        } catch (Exception e) {
            log.error("Creator SeleniumDriver fail.", e);
            throw new CreateWebDriverException("Creator SeleniumDriver fail.", e);
        }
    }

    private SeleniumDriver getChromeWebDriver(SeleniumProperties seleniumProperties) throws IOException {
        ChromeOptions options = new ChromeOptions();

        // 代理服务器
        SeleniumProxy proxy = seleniumProperties.getProxy();
        if (proxy != null) {
            //--proxy-server="socks://127.0.0.1:1080" 格式如下：代理协议://代理地址:代理端口号
            options.addArguments("--proxy-server=" + String.format("%s://%s:%s", proxy.getScheme().name(), proxy.getHost(), proxy.getHost()));
        }

        // 用户数据目录
        String userDataDir = Files.createTempDirectory("chrome_driver.").toString();
        options.addArguments("--user-data-dir=" + userDataDir);

        // 语言
        String language = Locale.getDefault().getLanguage().replace("_", "-");
        options.addArguments("--lang=" + language);

        if (Mode.UNDETECTED.equals(seleniumProperties.getMode())) {
            URL remoteAddress = new URL(seleniumProperties.getRemoteUrl());
            Process process;
            // 启动浏览器进程
            try {
                List<String> args = new ArrayList<>();
                args.add(seleniumProperties.getBinaryLocation());
                args.add("--remote-allow-origins=*");
                args.add("--remote-debugging-host=" + remoteAddress.getHost());
                args.add("--remote-debugging-port=" + remoteAddress.getPort());
                args.add("--user-data-dir=" + userDataDir);
                args.add("--lang=" + language);
                args.add("--no-default-browser-check");
                args.add("--no-first-run");
                args.add("--log-level=0");
                process = new ProcessBuilder(args).start();
            } catch (Exception e) {
                log.error("Launch Browser BinaryLocation fail.", e);
                throw new LaunchBrowserException("浏览器进程启动出错.", e);
            }

            // 连接浏览器进程
            options.setBinary(seleniumProperties.getBinaryLocation());
            options.addArguments("--remote-debugging-host=" + remoteAddress.getHost());
            options.addArguments("--remote-debugging-port=" + remoteAddress.getPort());
            options.addArguments("--no-default-browser-check");
            options.addArguments("--no-first-run");
            options.addArguments("--log-level=0");
            options.setExperimentalOption("debuggerAddress", remoteAddress.getHost() + ":" + remoteAddress.getPort());
            System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, seleniumProperties.getDriver());
            ChromeDriver webDriver = new ChromeDriver(options);

            // 抹除Selenium特征，绕开防爬虫监测
            if (seleniumProperties.getCamouflageEnabled()) {
                try {
                    ClassPathResource stealthExtendJsResource = new ClassPathResource("org/luxor/commons/selenium/js/stealth.extend.js");
                    String stealthExtendJs = IOUtils.toString(stealthExtendJsResource.getInputStream(), StandardCharsets.UTF_8);
                    Map<String, Object> uaMap = new HashMap<String, Object>(2) {{
                        put("source", stealthExtendJs);
                    }};
                    webDriver.executeCdpCommand("Page.addScriptToEvaluateOnNewDocument", uaMap);
                } catch (Exception e) {
                    throw new StealthJsException("爬虫特征码抹除脚本(stealth.extend.js),执行失败", e);
                }
            }
            return new SeleniumDriver(webDriver, seleniumProperties, process, userDataDir);
        }

        // 设置页面加载策略
        options.setPageLoadStrategy(seleniumProperties.getPageLoadStrategy());
        // 设置成开发者模式
        options.setExperimentalOption("excludeSwitches", Collections.singletonList("enable-automation"));

        // 除去window.navigator.webdriver，否则调用远程webdriver时则会报错
        options.addArguments("--disable-blink-features");
        options.addArguments("--disable-blink-features=AutomationControlled");

        // 启用无头模式
        if (Mode.CMD.equals(seleniumProperties.getMode()) || Mode.CLUSTER.equals(seleniumProperties.getMode())) {
            options.addArguments("--headless");
            options.addArguments("--disable-gpu");
        }

        // 禁止加载图片
        if (Boolean.FALSE.equals(seleniumProperties.getImagesEnabled())) {
            options.addArguments("--blink-settings=imagesEnabled=false");
        }

        // 禁用密码保存
        Map<String, Object> prefs = new HashMap<>(2);
        prefs.put("credentials_enable_service", false);
        prefs.put("profile.password_manager_enabled", false);
        // 禁用自动下载
        prefs.put("download.prompt_for_download", false);
        // 设置下载目录
        prefs.put("download.default_directory", seleniumProperties.getDownloadDirectory());
        options.setExperimentalOption("prefs", prefs);

        // 模拟真实浏览器请求头
        options.addArguments("sec-ch-ua=\"Chromium\";v=\"112\", \"Google Chrome\";v=\"112\", \"Not:A-Brand\";v=\"99\"");
        options.addArguments("sec-ch-ua-mobile=?0");
        options.addArguments("sec-ch-ua-platform=\"macOS\"");
        options.addArguments("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36");

        // 取消沙盒模式
        options.addArguments("--no-sandbox");
        options.addArguments("--disable-setuid-sandbox");
        // 禁用插件
        options.addArguments("--disable-plugins");
        // Disable CSS
        options.addArguments("--disable-stylesheets");
        // 取消提示信息
        options.addArguments("--disable-infobars");

        RemoteWebDriver webDriver;
        if (Mode.CLUSTER.equals(seleniumProperties.getMode())) {
            URL remoteAddress = new URL(seleniumProperties.getRemoteUrl());
            webDriver = new RemoteWebDriver(remoteAddress, options);
            MutableCapabilities mutableCapabilities = ((MutableCapabilities) webDriver.getCapabilities());
            String wsUrl = "ws://" + remoteAddress.getHost() + ":" + remoteAddress.getPort() + "/session/" + webDriver.getSessionId() + "/se/cdp";
            mutableCapabilities.setCapability("se:cdp", wsUrl);
            webDriver.setFileDetector(new LocalFileDetector());
        } else {
            System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, seleniumProperties.getDriver());
            webDriver = new ChromeDriver(options);
        }

        // 抹除Selenium特征，绕开防爬虫监测
        if (seleniumProperties.getCamouflageEnabled()) {
            String commandName = "Page.addScriptToEvaluateOnNewDocument";
            ExecuteMethod executeMethod = new RemoteExecuteMethod(webDriver);
            try {
                // https://github.com/berstend/puppeteer-extra/tree/master/packages/extract-stealth-evasions
                // https://raw.githubusercontent.com/requireCool/stealth.min.js/main/stealth.min.js
                ClassPathResource stealthJsResource = new ClassPathResource("org/luxor/commons/selenium/js/stealth.min.js");
                String stealthJs = IOUtils.toString(stealthJsResource.getInputStream(), StandardCharsets.UTF_8);
                Map<String, Object> uaMap = new HashMap<String, Object>(2) {{
                    put("source", stealthJs);
                }};
                executeMethod.execute("executeCdpCommand", ImmutableMap.of("cmd", commandName, "params", uaMap));
            } catch (Exception e) {
                throw new StealthJsException("爬虫特征码抹除脚本(stealth.min.js),执行失败", e);
            }
            try {
                ClassPathResource stealthExtendJsResource = new ClassPathResource("org/luxor/commons/selenium/js/stealth.extend.js");
                String stealthExtendJs = IOUtils.toString(stealthExtendJsResource.getInputStream(), StandardCharsets.UTF_8);
                Map<String, Object> uaMap = new HashMap<String, Object>(2) {{
                    put("source", stealthExtendJs);
                }};
                executeMethod.execute("executeCdpCommand", ImmutableMap.of("cmd", commandName, "params", uaMap));
            } catch (Exception e) {
                throw new StealthJsException("爬虫特征码抹除脚本(stealth.extend.js),执行失败", e);
            }
        }
        return new SeleniumDriver(webDriver, seleniumProperties, null, userDataDir);
    }

    private SeleniumDriver getFirefoxWebDriver(SeleniumProperties seleniumProperties) throws IOException {
        RemoteWebDriver webDriver;
        System.setProperty(GeckoDriverService.GECKO_DRIVER_EXE_PROPERTY, seleniumProperties.getDriver());
        FirefoxOptions options = new FirefoxOptions();

        // 代理服务器
        SeleniumProxy proxy = seleniumProperties.getProxy();
        if (proxy != null) {
            //--proxy-server="socks://127.0.0.1:1080" 格式如下：代理协议://代理地址:代理端口号
            options.addArguments("--proxy-server=" + String.format("%s://%s:%s", proxy.getScheme().name(), proxy.getHost(), proxy.getHost()));
        }
        // 用户数据目录
        String userDataDir = Files.createTempDirectory("firefox_driver.").toString();
        options.addArguments("--user-data-dir=" + userDataDir);
        // 语言
        String language = Locale.getDefault().getLanguage().replace("_", "-");
        options.addArguments("--lang=" + language);

        // 设置页面加载策略
        options.setPageLoadStrategy(seleniumProperties.getPageLoadStrategy());
        if (Mode.CMD.equals(seleniumProperties.getMode())) {
            options.addArguments("--headless");
            options.addArguments("--disable-gpu");
        }
        if (Boolean.FALSE.equals(seleniumProperties.getImagesEnabled())) {
            options.addArguments("--blink-settings=imagesEnabled=false");
        }
        if (Mode.CLUSTER.equals(seleniumProperties.getMode())) {
            webDriver = new RemoteWebDriver(new URL(seleniumProperties.getRemoteUrl()), options);
        } else {
            System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, seleniumProperties.getDriver());
            webDriver = new FirefoxDriver(options);
        }
        return new SeleniumDriver(webDriver, seleniumProperties, null, userDataDir);
    }

    private SeleniumDriver getInternetExplorerWebDriver(SeleniumProperties seleniumProperties) throws IOException {
        RemoteWebDriver webDriver;
        if (Mode.CMD.equals(seleniumProperties.getMode())) {
            throw new SeleniumException("InternetExplorerDriver 不支持无界面,运行模式");
        }
        InternetExplorerOptions options = new InternetExplorerOptions();

        // 设置页面加载策略
        options.setPageLoadStrategy(seleniumProperties.getPageLoadStrategy());
        if (Mode.CLUSTER.equals(seleniumProperties.getMode())) {
            webDriver = new RemoteWebDriver(new URL(seleniumProperties.getRemoteUrl()), options);
        } else {
            System.setProperty(InternetExplorerDriverService.IE_DRIVER_EXE_PROPERTY, seleniumProperties.getDriver());
            webDriver = new InternetExplorerDriver(options);
        }
        return new SeleniumDriver(webDriver, seleniumProperties, null, null);
    }

    private SeleniumDriver getEdgeWebDriver(SeleniumProperties seleniumProperties) throws IOException {
        RemoteWebDriver webDriver;
        if (Mode.CMD.equals(seleniumProperties.getMode())) {
            throw new SeleniumException("EdgeDriver 不支持无界面,运行模式");
        }
        EdgeOptions options = new EdgeOptions();

        // 代理服务器
        SeleniumProxy proxy = seleniumProperties.getProxy();
        if (proxy != null) {
            //--proxy-server="socks://127.0.0.1:1080" 格式如下：代理协议://代理地址:代理端口号
            options.addArguments("--proxy-server=" + String.format("%s://%s:%s", proxy.getScheme().name(), proxy.getHost(), proxy.getHost()));
        }
        // 用户数据目录
        String userDataDir = Files.createTempDirectory("edge_driver.").toString();
        options.addArguments("--user-data-dir=" + userDataDir);
        // 语言
        String language = Locale.getDefault().getLanguage().replace("_", "-");
        options.addArguments("--lang=" + language);

        // 设置页面加载策略
        options.setPageLoadStrategy(seleniumProperties.getPageLoadStrategy());
        if (Mode.CLUSTER.equals(seleniumProperties.getMode())) {
            webDriver = new RemoteWebDriver(new URL(seleniumProperties.getRemoteUrl()), options);
        } else {
            System.setProperty(EdgeDriverService.EDGE_DRIVER_EXE_PROPERTY, seleniumProperties.getDriver());
            webDriver = new EdgeDriver(options);
        }
        return new SeleniumDriver(webDriver, seleniumProperties, null, userDataDir);
    }

    private SeleniumDriver getSafariWebDriver(SeleniumProperties seleniumProperties) throws IOException {
        RemoteWebDriver webDriver;
        if (Mode.CMD.equals(seleniumProperties.getMode())) {
            throw new CreateWebDriverException("SafariDriver 不支持无界面,运行模式");
        }
        SafariOptions options = new SafariOptions();
        // 设置页面加载策略
        options.setCapability(PAGE_LOAD_STRATEGY, seleniumProperties.getPageLoadStrategy());
        if (Mode.CLUSTER.equals(seleniumProperties.getMode())) {
            webDriver = new RemoteWebDriver(new URL(seleniumProperties.getRemoteUrl()), options);
        } else {
            System.setProperty(EdgeDriverService.EDGE_DRIVER_EXE_PROPERTY, seleniumProperties.getDriver());
            webDriver = new SafariDriver(options);
        }
        return new SeleniumDriver(webDriver, seleniumProperties, null, null);
    }

}

